<?php

namespace butpay\wechat;

/**
 * 微信支付V3版接口集合
 */
class WxPay
{
    //V3 Authorization 微信指定认证类型
    const SCHEMA = 'WECHATPAY2-SHA256-RSA2048';

    /**
     * 下单
     *
     * @param string $orderNo 订单号
     * @param string $openId 微信用户openid
     * @param integer $amount 订单金额，单位分
     * @param string $desc 订单描述
     * @param string $appId 应用appId
     * @param string $mchId 微信商户号
     * @param string $notifyUrl 支付结果通知地址
     * @param string $certPemPath 微信支付证书文件路径
     * @param string $keyPemPath 微信支付密钥文件路径
     * @return array
     */
    public static function jsapi(
        string $orderNo,
        string $openId,
        int $amount,
        string $desc,
        string $appId,
        string $mchId,
        string $notifyUrl,
        string $certPemPath,
        string $keyPemPath
    ): array {

        $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi';

        $postData = [
            'appid'        => $appId,
            'mchid'        => $mchId,
            'description'  => $desc,
            'out_trade_no' => $orderNo,
            'notify_url'   => $notifyUrl,
            'amount' => [
                'total' => $amount
            ],
            'payer' => [
                'openid' => $openId
            ]
        ];
        //构造请求头TOKEN
        $headerToken = self::generateHeaderToken($url, $postData, 'POST', $keyPemPath, $certPemPath, $mchId);
        //发送请求
        $res = self::curlRequest($url, $headerToken, 1, $postData);

        $result = json_decode($res, true);

        if (!empty($result) && isset($result['prepay_id'])) {
            $timestamp = time();
            $nonce     = self::getNonceStr();
            $package   = 'prepay_id=' . $result['prepay_id'];

            $signStr = $appId . "\n" .
                $timestamp . "\n" .
                $nonce . "\n" .
                $package . "\n";

            return [
                'appId'     => $appId,
                'timeStamp' => (string)$timestamp,
                'nonceStr'  => $nonce,
                'package'   => $package,
                'signType'  => 'RSA',
                'paySign'   => self::makeSign($signStr, $keyPemPath)
            ];
        }
        return [];
    }

    /**
     * Native 下单
     *
     * @param string $orderNo 订单号
     * @param integer $amount 订单金额，单位分
     * @param string $desc 订单描述
     * @param string $appId 应用appId
     * @param string $mchId 微信商户号
     * @param string $notifyUrl 支付结果通知地址
     * @param string $certPemPath 微信支付证书文件路径
     * @param string $keyPemPath 微信支付密钥文件路径
     * @return array
     */
    public static function native(
        string $orderNo,
        int $amount,
        string $desc,
        string $appId,
        string $mchId,
        string $notifyUrl,
        string $certPemPath,
        string $keyPemPath
    ): array {
        $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/native';

        $postData = [
            'appid'        => $appId,
            'mchid'        => $mchId,
            'description'  => $desc,
            'out_trade_no' => $orderNo,
            'notify_url'   => $notifyUrl,
            'amount' => [
                'total' => $amount
            ]
        ];
        //构造请求头TOKEN
        $headerToken = self::generateHeaderToken($url, $postData, 'POST', $keyPemPath, $certPemPath, $mchId);
        //发送请求
        $res = self::curlRequest($url, $headerToken, 1, $postData);

        $result = json_decode($res, true);

        if (!empty($result) && isset($result['code_url'])) {
            return [
                'code_url' => $result['code_url']
            ];
        }
        return [];
    }

    public static function queryOrder(string $orderNo, string $mchId, string $certPemPath, string $keyPemPath): array
    {
        $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/' . $orderNo . '?mchid=' . $mchId;

        $postData = [];

        $headerToken = self::generateHeaderToken($url, $postData, 'GET', $keyPemPath, $certPemPath, $mchId);

        $res = self::curlRequest($url, $headerToken, 0, $postData);

        $result = json_decode($res, true);

        if (!empty($result)) {
            return $result;
        }
        return [];
    }

    public static function closeOrder(string $orderNo, string $mchId, string $certPemPath, string $keyPemPath): bool
    {
        $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/' . $orderNo . '/close';

        $postData = [
            'mchid' => $mchId
        ];

        $headerToken = self::generateHeaderToken($url, $postData, 'POST', $keyPemPath, $certPemPath, $mchId);

        $res = self::curlRequest($url, $headerToken, 1, $postData);

        return $res === '';
    }

    public static function refundOrder(
        string $mchId,
        string $orderNo,
        string $refundNo,
        int $totalAmount,
        int $refundAmount,
        string $notifyUrl,
        string $certPemPath,
        string $keyPemPath
    ): array {

        $url = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';

        $postData = [
            'out_trade_no' => $orderNo,
            'out_refund_no' => $refundNo,
            'notify_url'   => $notifyUrl,
            'amount' => [
                'refund'   => $refundAmount,
                'total'    => $totalAmount,
                'currency' => 'CNY'
            ]
        ];

        $headerToken = self::generateHeaderToken($url, $postData, 'POST', $keyPemPath, $certPemPath, $mchId);

        $res = self::curlRequest($url, $headerToken, 1, $postData);

        $result = json_decode($res, true);

        return $result;
    }

    /**
     * 构造微信回调应答（支付回调、退款回调）
     *
     * @param string $code 返回状态码
     * @param string $msg 返回信息
     * @param integer $httpCode 应答http状态码，成功为200，失败为5xx或4xx
     * @return object
     */
    public static function notifyResponse(string $code = 'SUCCESS', string $msg = 'OK', int $httpCode = 200): object
    {
        return json(['code' => $code, 'message' => $msg], $httpCode);
    }

    /**
     * 构造请求头TOKEN
     *
     * @param string $url 请求URL
     * @param array $postData 请求参数
     * @param string $httpMethod 请求方式
     * @param string $keyPemPath 微信商户密钥文件路径
     * @param string $certPemPath 微信商户证书密钥文件路径
     * @param string $mchId 微信商户号
     * @return string
     */
    private static function generateHeaderToken(
        string $url,
        array $postData,
        string $httpMethod,
        string $keyPemPath,
        string $certPemPath,
        string $mchId
    ): string {

        $timestamp = time();
        $nonce     = self::getNonceStr();

        $body      = !empty($postData) ? json_encode($postData) : '';

        $url_parts = parse_url($url);
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));

        $headerSignParams = $httpMethod . "\n" .
            $canonical_url . "\n" .
            $timestamp . "\n" .
            $nonce . "\n" .
            $body . "\n";

        return sprintf(
            'mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $mchId,
            $nonce,
            $timestamp,
            self::getPemSerialNo($certPemPath),
            self::makeSign($headerSignParams, $keyPemPath)
        );
    }
    /**
     * 生成签名
     *
     * @param string $signParams 签名参数字符串
     * @param string $keyPemPath 微信商户密钥文件路径
     * @return string
     */
    private static function makeSign(string $signParams, string $keyPemPath): string
    {
        openssl_sign($signParams, $sign, file_get_contents($keyPemPath), 'sha256WithRSAEncryption');
        return base64_encode($sign);
    }

    /**
     * 获取微信商户证书序列号
     *
     * @param string $certPemPath 微信商户证书文件路径
     * @return string
     */
    private static function getPemSerialNo(string $certPemPath): string
    {
        //获取证书序列号
        $serialNo = openssl_x509_parse(file_get_contents($certPemPath));
        return strtoupper($serialNo['serialNumberHex']);
    }

    /**
     * 生成随机字符串
     *
     * @param integer $length
     * @return string
     */
    private static function getNonceStr(int $length = 32): string
    {
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str = '';
        for ($i = 0; $i < $length; $i++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }

    /**
     * 自定义发送请求方法
     *
     * @param string $url 请求URL
     * @param string $headerToken 请求header token
     * @param boolean $isPost 是否post请求
     * @param array $postData post 请求参数
     * @return string
     */
    private static function curlRequest(string $url, string $headerToken, bool $isPost = false, array $postData = []): string
    {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_TIMEOUT, 60);
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_HEADER, 0);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);

        //添加header
        $header = [
            'Content-Type: application/json',
            'Accept: application/json',
            'Authorization:' . self::SCHEMA . ' ' . $headerToken,
            'User-Agent:' . $_SERVER['HTTP_USER_AGENT']
        ];
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);

        if ($isPost) {
            curl_setopt($curl, CURLOPT_POST, TRUE);
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($postData));
        }

        $res = curl_exec($curl);
        if ($res === false) {
            $res = 'Curl error: ' . curl_error($curl);
        }

        curl_close($curl);

        return $res;
    }

    /**
     * 回调通知Aes解密  回调通知加密算法为 AEAD_AES_256_GCM
     *
     * @param string    $associatedData     AES GCM additional authentication data
     * @param string    $nonceStr           AES GCM nonce
     * @param string    $ciphertext         AES GCM cipher text
     *
     * @return string|bool      Decrypted string on success or FALSE on failure
     */
    public static function aesDecryptToString($aesKey, $associatedData, $nonceStr, $ciphertext)
    {
        $authTagLengthByte = 16;
        if (strlen($aesKey) != 32) {
            return false;
        }

        $ciphertext = \base64_decode($ciphertext);
        if (strlen($ciphertext) <= $authTagLengthByte) {
            return false;
        }

        $ctext = substr($ciphertext, 0, -$authTagLengthByte);
        $authTag = substr($ciphertext, -$authTagLengthByte);

        return \openssl_decrypt(
            $ctext,
            'aes-256-gcm',
            $aesKey,
            \OPENSSL_RAW_DATA,
            $nonceStr,
            $authTag,
            $associatedData
        );
    }
}
